<?php
namespace redislock;

class Key
{
    /** @var string */
    private $key;

    /** @var string */
    private $token;

    public function __construct(string $key)
    {
        $this->key = $key;
    }

    public function __toString()
    {
        return $this->key;
    }

    public function getToken()
    {
        if ($this->token !== null) {
            return $this->token;
        }

        return $this->token = base64_encode(random_bytes(32));
    }
}

class RedisLock
{
    /** @var \Redis */
    private $redis;

    /** @var array */
    private $locks;

    const lock_prefix = "lock:";
    const lock_interval = 25;

    public function __construct(\Redis $redis)
    {
        $this->locks = array();
        $this->redis = $redis;
    }

    /**
     * 根据字符串创建一个锁对象
     * @param string $string
     * @return Key
     */
    private function createLockKey(string $string)
    {
        return new Key($string);
    }

    private function acquire(Key $key, int $seconds = 60): bool
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("PEXPIRE", KEYS[1], ARGV[2])
            else
                return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
            end
        ';

        return $this->redis->eval($script, [(string)$key, $key->getToken(), $seconds * 1000], 1);
    }

    private function release(Key $key)
    {
        $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';

        return $this->redis->eval($script, [(string)$key, $key->getToken()], 1);
    }

    /**
     * 获取一个锁
     * @param string $string    //锁唯一标识字符串
     * @param int $timeout      //锁最长生存时间(秒)
     * @param int $waitout      //等待锁最长时间(秒)
     * @return bool
     */
    public function acquireEx(string $string, int $timeout, int $waitout)
    {
        $lockKey = $this->createLockKey(self::lock_prefix . $string);
        $waitoutAt = time() + $waitout;
        while (true) {
            if ($this->acquire($lockKey, $timeout)) {
                $this->locks[$string] = $lockKey;
                return true;
            }
            if ($waitout <= 0 || time() > $waitoutAt) break;
            usleep(self::lock_interval * 1000);
        }
        return false;
    }

    /**
     * 释放一个锁
     * @param string $string
     * @return bool
     */
    public function releaseEx(string $string)
    {
        if (isset($this->locks[$string])){
            if ($this->release($this->locks[$string])){
                unset($this->locks[$string]);
                return true;
            }
        }
        return false;
    }

    /**
     * 释放所有锁
     * @return bool
     */
    public function releaseAll()
    {
        $allStatus = true;
        foreach ($this->locks as $key => $__) {
            $allStatus &= $this->releaseEx($key);
        }
        return $allStatus;
    }
}